// This Pine Script™ code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © fluxchart
//@version=5

const bool DEBUG = false
const int maxBoxesCount = 500

indicator(title = 'Inversion Fair Value Gap Oscillator | Flux Charts', shorttitle = 'IFVG Oscillator | Flux Charts', overlay = false, max_boxes_count = maxBoxesCount, max_labels_count = maxBoxesCount, max_lines_count = maxBoxesCount, precision = 2)

const int showLastXFVGs = 50
const int maxDistanceToLastBar = 20000
const int atrLen = 10
const int labelCooldown = 10
const float maxGradientVal = 2.5

decayCoeff = input.float(0.03, "Decay Coefficient", minval = 0.01, maxval = 0.99, step = 0.01, group = "General Configuration", display = display.none)
showDivergences = input.bool(true, "Show Divergences", group = "General Configuration", display = display.none)
showFVGs = input.bool(false, "Show IFVGs", group = "General Configuration", display = display.none)

fvgEnabled = true
fvgEndMethod = input.string("Wick", "Zone Invalidation", options = ["Wick", "Close"],  group = "Fair Value Gaps")
fvgFilterMethod = input.string("Average Range", "Zone Filtering", options = ["Average Range", "Volume Threshold"],  group = "Fair Value Gaps")
volumeThresholdPercent = input.int(50, "Volume Threshold %", group = "Fair Value Gaps", tooltip = "Only taken into calculation when filter method is Volume Threshold.", minval = 1, maxval = 200)
fvgBars = input.string("All", "FVG Detection", options = ["Same Type", "All"], tooltip = "Same Type -> All 3 bars that formed the FVG should be the same type. (Bullish / Bearish) \n\nAll -> Bar types may vary between bullish / bearish.", group = "Fair Value Gaps")
fvgSensEnabled = true
fvgSensitivityText = input.string("Extreme", "Detection Sensitivity", inline = "sens", options = ["Extreme", "High", "Normal", "Low"], group = "Fair Value Gaps")
allowGaps = input.bool(false, "Allow Gaps Between Bars", group = "Fair Value Gaps", tooltip = "On tickers that can have a different opening price than the previous bar's closing price, the indicator will not analyze those bars for FVGs if disabled.\n\nFor Example, if the today's opening price is different from the yesterday's closing price in a stock ticker.")

ifvgEndMethod = input.string("Wick", "IFVG Zone Invalidation", options = ["Wick", "Close"],  group = "Inversion Fair Value Gaps")

divergenceLabelsOverlay = input.string("Oscillator", "Divergence Labels On", ["Oscillator", "Chart"], group = 'Style', display = display.none)
fvgBullColor = input(#08998180, 'Bullish', inline = 'fvgColor', group = 'Style', display = display.none)
neutralColor = input(color.gray, 'Neutral', inline = 'fvgColor', group = 'Style', display = display.none)
fvgBearColor = input(#f2364680, 'Bearish', inline = 'fvgColor', group = 'Style', display = display.none)

ifvgEnabled = false //input.bool(false, "Enabled", inline="EV", group = "Inversion Fair Value Gaps")
ifvgFull = DEBUG ? input.bool(true, "IFVG Full", group = "Inversion Fair Value Gaps", display = display.none) : true
bullishInverseColor = #08998180//input(#08998180, "Bullish", inline = 'breakerColor', group = 'Inversion Fair Value Gaps', display = display.none)
bearishInverseColor = #f2364580//input(#f2364580, "Bearish", inline = 'breakerColor', group = 'Inversion Fair Value Gaps', display = display.none)

insideBounds = ((bar_index > last_bar_index - maxDistanceToLastBar) and barstate.isconfirmed)

fvgSensitivity = fvgSensitivityText == "Extreme" ? 6 : fvgSensitivityText == "High" ? 2 : fvgSensitivityText == "Normal" ? 1.5 : 1
atr = ta.atr(atrLen)
volCheck = (ta.cum(volume) > 0)

type FVGInfo
    float max = na
    float min = na
    bool isBull = na
    int t = na
    float totalVolume = na
    int startBarIndex = na
    int endBarIndex = na
    int startTime = na
    int endTime = na
    bool combined = false
    string combinedTimeframesStr = na
    bool disabled = false
    string timeframeStr = na

    float lowVolume = na
    float highVolume = na
    bool isInverse = false
    int lastTouched = na
    int lastTouchedIFVG = na
    int inverseEndIndex = na
    int inverseEndTime = na
    float inverseVolume

    box fvgBox = na
    bool calculated = false

createFVGInfo (h, l, bull, t, tv) =>
    FVGInfo newFVGInfo = FVGInfo.new(h, l, bull, t, tv)
    newFVGInfo

var FVGInfo[] FVGInfoList = array.new<FVGInfo>(0)

//#region Find FVGs
findFVGs () =>
    if insideBounds and (fvgEnabled or ifvgEnabled)
        shortVol = ta.sma(volume, 5)
        longVol = ta.sma(volume, 15)

        bearCondition = false
        bullCondition = false

        shortTerm = volCheck ? shortVol : 1
        longTerm = volCheck ? longVol : 0

        firstBarSize = math.max(open, close) - math.min(open, close)
        secondBarSize = math.max(open[1], close[1]) - math.min(open[1], close[1])
        thirdBarSize = math.max(open[2], close[2]) - math.min(open[2], close[2])
        barSizeSum = firstBarSize + secondBarSize + thirdBarSize

        barSizeCheck = true
        //if (secondBarSize < math.max(firstBarSize, thirdBarSize))
            //barSizeCheck := false

        fvgBarsCheck = false
        if fvgBars == "Same Type"
            if (open > close and open[1] > close[1] and open[2] > close[2]) or (open <= close and open[1] <= close[1] and open[2] <= close[2])
                fvgBarsCheck := true
        else
            fvgBarsCheck := true

        if fvgBarsCheck and barSizeCheck
            diffThreshold = atr / 20.0
            maxDiffBullish = math.max(low[1] > high[2] ? low[1] - high[2] : 0, low > high[1] ? low - high[1] : 0)
            maxDiffBearish = math.max(high[1] < low[2] ? low[2] - high[1] : 0, high < low[1] ? low[1] - high : 0)
            maxDiff = math.max(maxDiffBullish, maxDiffBearish)
            if fvgFilterMethod == "Average Range"
                bearCondition := ((not fvgSensEnabled) or (barSizeSum * fvgSensitivity > atr / 1.5)) and (allowGaps or (maxDiff <= diffThreshold))
                bullCondition := ((not fvgSensEnabled) or (barSizeSum * fvgSensitivity > atr / 1.5)) and (allowGaps or (maxDiff <= diffThreshold))
            else if fvgFilterMethod == "Volume Threshold"
                thresholdMultiplier = (volumeThresholdPercent / 100.0)
                bearCondition := shortTerm > longTerm * thresholdMultiplier and (allowGaps or (maxDiff <= diffThreshold))
                bullCondition := shortTerm > longTerm * thresholdMultiplier and (allowGaps or (maxDiff <= diffThreshold))

        bearFVG = high < low[2] and close[1] < low[2] and bearCondition
        bullFVG = low > high[2] and close[1] > high[2] and bullCondition

        volSum3 = math.sum(volume, 3)
        float totalVolume = volCheck ? volSum3 : 0
        FVGInfo newFVGInfo = bearFVG ? createFVGInfo(low[2], high, false, time, totalVolume) : bullFVG ? createFVGInfo(low, high[2], true, time, totalVolume) : na
        FVGSize = bearFVG ? math.abs(low[2] - high) : bullFVG ? math.abs(low - high[2]) : 0

        FVGSizeEnough = (FVGSize * fvgSensitivity > atr)
        if FVGSizeEnough
            if not na(newFVGInfo)
                newFVGInfo.startTime := time
                newFVGInfo.startBarIndex := bar_index
                newFVGInfo.lastTouched := bar_index
                if bearFVG
                    newFVGInfo.lowVolume := volCheck ? (volume + volume[1]) : 0
                    newFVGInfo.highVolume := volCheck ? volume[2] : 0
                else
                    newFVGInfo.lowVolume := volCheck ? volume[2] : 0
                    newFVGInfo.highVolume := volCheck ? (volume + volume[1]) : 0

            if not na(newFVGInfo)
                FVGInfoList.unshift(newFVGInfo)
                while FVGInfoList.size() > showLastXFVGs
                    popped = FVGInfoList.pop()
                    box.delete(popped.fvgBox)

        // Find Closed FVGs
        if FVGInfoList.size () > 0
            for i = 0 to FVGInfoList.size() - 1
                curFVG = FVGInfoList.get(i)
                // Is Touched FVG
                if ((curFVG.isBull) and low <= curFVG.max) or ((not curFVG.isBull) and high >= curFVG.min)
                    curFVG.lastTouched := bar_index
                
                if ((not curFVG.isBull) and low <= curFVG.max) or ((curFVG.isBull) and high >= curFVG.min)
                    curFVG.lastTouchedIFVG := bar_index  

                // IFVG Close
                if curFVG.isInverse and na(curFVG.inverseEndIndex) and time > curFVG.endTime
                    if (not curFVG.isBull) and (ifvgEndMethod == "Wick" ? low < curFVG.min : close < curFVG.min)
                        curFVG.inverseEndIndex := bar_index
                        curFVG.inverseEndTime := time
                    
                    if curFVG.isBull and (ifvgEndMethod == "Wick" ? high > curFVG.max : close > curFVG.max)
                        curFVG.inverseEndIndex := bar_index
                        curFVG.inverseEndTime := time
                
                if na(curFVG.endBarIndex)
                    // FVG End
                    if curFVG.isBull and (fvgEndMethod == "Wick" ? low < curFVG.min : close < curFVG.min)
                        curFVG.endBarIndex := bar_index
                        curFVG.endTime := time
                        curFVG.isInverse := true
                        curFVG.inverseVolume := nz(volume)
                        curFVG.lastTouchedIFVG := bar_index
                    
                    if (not curFVG.isBull) and (fvgEndMethod == "Wick" ? high > curFVG.max : close > curFVG.max)
                        curFVG.endBarIndex := bar_index
                        curFVG.endTime := time
                        curFVG.isInverse := true
                        curFVG.inverseVolume := nz(volume)
                        curFVG.lastTouchedIFVG := bar_index
    FVGInfoList
findFVGs()
//#endregion

var fvgOscBull = 0.0
var fvgOscBear = 0.0
var fvgOscNet = 0.0

float drawBullChar = na
float drawBearChar = na

if barstate.isconfirmed
    if FVGInfoList.size() > 0
        for i = 0 to FVGInfoList.size() - 1
            curFVGInfo = FVGInfoList.get(i)
            if curFVGInfo.isInverse
                if not curFVGInfo.calculated
                    if not curFVGInfo.isBull
                        val = math.abs(curFVGInfo.max - curFVGInfo.min) / atr
                        fvgOscBull += val
                        drawBullChar := val
                    else
                        val = math.abs(curFVGInfo.max - curFVGInfo.min) / atr
                        fvgOscBear -= val
                        drawBearChar := -val
                    curFVGInfo.calculated := true
    fvgOscBull *= (1.0 - decayCoeff)
    fvgOscBear *= (1.0 - decayCoeff)
    fvgOscNet := fvgOscBull + fvgOscBear

plotchar(drawBullChar, "", location = location.absolute, char = "-", color = color.new(fvgBullColor, 0), size = size.tiny)
plotchar(drawBearChar, "", location = location.absolute, char = "-", color = color.new(fvgBearColor, 0), size = size.tiny)

zeroline = hline(0, color = color.new(neutralColor, 50), linestyle = hline.style_dashed)
zeroplot = plot(0, display = display.none)
bullPlot = plot(fvgOscBull, color = color.new(fvgBullColor, 0))
bearPlot = plot(fvgOscBear, color = color.new(fvgBearColor, 0))
plot(fvgOscNet, color = neutralColor)
fill(zeroplot, bullPlot, top_value = maxGradientVal, bottom_value = 0, top_color = fvgBullColor, bottom_color = color.new(fvgBullColor, 100))
fill(zeroplot, bearPlot, top_value = 0, bottom_value = -maxGradientVal, top_color = color.new(fvgBearColor, 100), bottom_color = fvgBearColor)

oscDiff = 0.3
pivotLen = 25

//oscDiff = input.float(0.3, "OSC Diff", group = "DEBUG")
//pivotLen = input.int(25, "Pivot Len", group = "DEBUG")

var lastHighLabel = 0
var lastLowLabel = 0
if showDivergences
    if (ta.lowest(pivotLen) == low) and (math.abs(ta.highest(fvgOscBull, pivotLen) - fvgOscBull)) <= oscDiff and fvgOscNet > 0 and bar_index > lastHighLabel + labelCooldown
        if divergenceLabelsOverlay == "Chart"
            label.new(time, low, "▲", yloc = yloc.belowbar, xloc = xloc.bar_time, style = label.style_label_up, color = fvgBullColor, textcolor = color.white, force_overlay = true)
        else
            label.new(time, fvgOscBear, "▲", yloc = yloc.price, xloc = xloc.bar_time, style = label.style_label_up, color = fvgBullColor, textcolor = color.white, force_overlay = false)
        lastHighLabel := bar_index

    if (ta.highest(pivotLen) == high) and (math.abs(ta.lowest(fvgOscBear, pivotLen) - fvgOscBear)) <= oscDiff and fvgOscNet < 0 and bar_index > lastLowLabel + labelCooldown
        if divergenceLabelsOverlay == "Chart"
            label.new(time, high, "▼", yloc = yloc.abovebar, xloc = xloc.bar_time, style = label.style_label_down, color = fvgBearColor, textcolor = color.white, force_overlay = true)
        else
            label.new(time, fvgOscBull, "▼", yloc = yloc.price, xloc = xloc.bar_time, style = label.style_label_down, color = fvgBearColor, textcolor = color.white, force_overlay = false)
        lastLowLabel := bar_index

alertcondition(lastHighLabel == bar_index, "Bullish Divergence")
alertcondition(lastLowLabel == bar_index, "Bearish Divergence")

if barstate.islast and showFVGs
    if FVGInfoList.size() > 0
        for i = 0 to FVGInfoList.size() - 1
            curFVGInfo = FVGInfoList.get(i)
            box.delete(curFVGInfo.fvgBox)
            if curFVGInfo.isInverse
                curFVGInfo.fvgBox := box.new(curFVGInfo.startTime, curFVGInfo.max, nz(curFVGInfo.inverseEndTime, time), curFVGInfo.min, xloc = xloc.bar_time, bgcolor = curFVGInfo.isBull ? fvgBearColor : fvgBullColor, border_color = na, force_overlay = true)